#!/bin/sh
#              -*- mode: sh; indent-tabs-mode:nil; sh-basic-offset:2 -*-
#
# An implementation of the sha256sum using only POSIX shell and
# POSIX utilities.
#
#   Requires: sed, nl, xargs, printf, mktemp, stat, od, bc
#
# Copyright © 2020 Eric Bavier <bavier@posteo.net>
# Update: Tue 18 Feb 2020 05:12:46 PM CST
# Update: Fri 06 Mar 2020 12:15:50 AM CST, rewrite w/ printf, od, and bc
# Update: Sat 07 Mar 2020 01:22:20 AM CST, speed up bc bitwise operations
# Update: Fri 03 Apr 2020 06:38:59 PM CDT, fix msg sched init and var shadowing
# Update: Thu 09 Apr 2020 10:54:39 PM CDT, accept multiple filename arguments
# Update: Thu 07 Jan 2021 11:37:34 PM CST, small speedup with precomputed 2^n
# License: GPLv3+
#####

# Note 0: Implementation from https://en.wikipedia.org/wiki/SHA-2#Pseudocode
# Note 1: All variables are 32 bit unsigned integers and addition is
#         calculated modulo 2^32
# Note 2: For each round, there is one round constant k[i] and one
#         entry in the message schedule array w[i], 0 ≤ i ≤ 63
# Note 3: The compression function uses 8 working variables, a through h
# Note 4: Big-endian convention is used when expressing the constants
#         in this pseudocode, and when parsing message block data from
#         bytes to words, for example, the first word of the input
#         message "abc" after padding is 0x61626380
# Note 5: This implementation is extremely inefficient...

fn_values_to_array(){
  sed 's/[^ ]\+/0x&/g
       s/  */\n/g' \
    | nl -v0 \
    | xargs printf "${1:0:1}[%d] = %d; "
}

fn_quote_hex(){
  sed 's/.*/0000000000000000&/  # left-pad so output is a full 64-bits
       s/.*\(.\{16\}\)/\1/      # trim to 16 nibbles
       :a
       s/^\([^ ]*\)\([^ ][^ ]\)/\1 \\\\x\2/
       t a
       s/^[^ ] /\\\\x& /'
}

constants=$(fn_values_to_array "k" <<EOF
   428a2f98 71374491 b5c0fbcf e9b5dba5 3956c25b 59f111f1 923f82a4 ab1c5ed5
   d807aa98 12835b01 243185be 550c7dc3 72be5d74 80deb1fe 9bdc06a7 c19bf174
   e49b69c1 efbe4786 0fc19dc6 240ca1cc 2de92c6f 4a7484aa 5cb0a9dc 76f988da
   983e5152 a831c66d b00327c8 bf597fc7 c6e00bf3 d5a79147 06ca6351 14292967
   27b70a85 2e1b2138 4d2c6dfc 53380d13 650a7354 766a0abb 81c2c92e 92722c85
   a2bfe8a1 a81a664b c24b8b70 c76c51a3 d192e819 d6990624 f40e3585 106aa070
   19a4c116 1e376c08 2748774c 34b0bcb5 391c0cb3 4ed8aa4a 5b9cca4f 682e6ff3
   748f82ee 78a5636f 84c87814 8cc70208 90befffa a4506ceb bef9a3f7 c67178f2
EOF
         )
values=$(mktemp -p "${TMPDIR:-.}") || exit 1; trap 'rm $values' EXIT

while test "$1" ; do
  # Initialize hash values:
  # (first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19):
  h0=$(printf "%d" 0x6a09e667)
  h1=$(printf "%d" 0xbb67ae85)
  h2=$(printf "%d" 0x3c6ef372)
  h3=$(printf "%d" 0xa54ff53a)
  h4=$(printf "%d" 0x510e527f)
  h5=$(printf "%d" 0x9b05688c)
  h6=$(printf "%d" 0x1f83d9ab)
  h7=$(printf "%d" 0x5be0cd19)

  size=$(stat --format="%s * 8" "$1" | bc)
  padding=$(printf "t=64-(($size/8)%%64);if(t<9){t+55}else{t-9}\n" | bc)
  (cat "$1";
   printf "\x80";
   printf "%.0s\x00" $(seq 1 $padding);
   printf "%x" $size | fn_quote_hex | xargs printf "%b") \
    | od --address-radix=none --endian=big \
         --format=x4 --width=64 --output-duplicates \
    | while read chunk ; do
    # CHUNK contains 16 64-bit hex values
    message_schedule=$(printf "$chunk\n" | fn_values_to_array "w")
    # Inject arrays into bc for the update step.
    bc >$values <<EOF
$constants
$message_schedule

scale = 0; ibase = A
j = 32      # bit-width
z = 2^32
t[0] = 1    # powers-of-two
for (i=1; i<=32; ++i)
  t[i] = t[i-1] * 2
define p(x,y){           /* plus modulo 2^32 */
  scale=0; return (x + y) % z; }
define g(x,n){           /* bitwise rightshift */
  scale=0; return x / t[n]; }
define f(x,n){           /* bitwise leftshift */
  scale=0; return (x * t[n]) % z; }
define n(x){             /* bitwise NOT */
  return z - 1 - x; }
define a(x,y){           /* bitwise AND */
  auto t, i, a, b, r
  t = 0; a = x; b = y; r = 1
  for (i=0; i<j; ++i) {
    t += (a % 2) * (b % 2) * r
    r *= 2; a /= 2; b /= 2
  }
  return t; }
define o(x,y){           /* bitwise OR */
  auto t, i, z, w, r, a, b
  t = 0; a = x; b = y; r = 1
  for (i=0; i<j; ++i) {
    z = a % 2; w = b % 2
    t += ((z + w + z*w) % 2) * r
    r *= 2; a /= 2; b /= 2
  }
  return t; }
define x(x, y){          /* bitwise XOR */
  auto t, i, a, b, r
  t = 0; a = x; b = y; r = 1
  for (i=0; i<j; ++i) {
    t += (((a % 2) + (b % 2)) % 2) * r
    r *= 2; a /= 2; b /= 2
  }
  return t; }
define r(x,n){           /* bitwise rightrotate */
  return o(f(x,j-n),g(x,n)); }

/* Extend first 16 words into the remaining 48 words w[16..63] of 
   the message schedule array: */
for (i=16; i<64; ++i) {
  s = x(x(r(w[i-15], 7),r(w[i-15],18)),g(w[i-15], 3))
  t = x(x(r(w[i- 2],17),r(w[i- 2],19)),g(w[i- 2],10))
  w[i] = p(p(w[i-16],s),p(w[i-7],t))
}
/* Initialize working variables to current hash value */
a=$h0; b=$h1; c=$h2; d=$h3; e=$h4; f=$h5; g=$h6; h=$h7
/* Compression function main loop: */
/* S1    is t
   ch    is y
   temp1 is u
   S0    is s
   maj   is m
   temp2 is v
 */
for (i=0; i<64; ++i) {
  t = x(r(e,6),x(r(e,11),r(e,25)))
  y = x(a(e,f),a(n(e),g))
  u = p(h,p(t,p(y,p(k[i],w[i]))))
  s = x(r(a,2),x(r(a,13),r(a,22)))
  m = x(a(a,b),x(a(a,c),a(b,c)))
  v = p(s,m)

  h = g
  g = f
  f = e
  e = p(d,u)
  d = c
  c = b
  b = a
  a = p(u,v)
}
print "h0=", p($h0,a), "\n"
print "h1=", p($h1,b), "\n"
print "h2=", p($h2,c), "\n"
print "h3=", p($h3,d), "\n"
print "h4=", p($h4,e), "\n"
print "h5=", p($h5,f), "\n"
print "h6=", p($h6,g), "\n"
print "h7=", p($h7,h), "\n"
EOF
    . $values                     # Update values for next loop
  done

  . $values                       # Import values from loop subprocess
  printf "%08x%08x%08x%08x%08x%08x%08x%08x  %s\n" \
         $h0 $h1 $h2 $h3 $h4 $h5 $h6 $h7   $1

  shift                         # Next input, if available
done
